iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0

前言

今天前半講認證與權限本身要決定什麼,後半講這一段交給 AI 做的時候,會遇到什麼、怎麼處理。


昨天說到

昨天處理掉 migration 的問題,今天要來處理系統的認證與權限。


認證與授權是兩件事

這兩個字常被混著講,但它們問的問題不同:

問什麼 這個系統的答案
認證(Authentication) 你是誰 以前:你說了算。現在:伺服器從簽章解出來
授權(Authorization) 你可以做什麼 你是不是這份記錄的作者、在不在這個版本的確認名單裡

JWT

傳統的 session 做法是:伺服器記住「這個 session id 對應到誰」,每次請求拿 id 去查。JWT 反過來——把身分寫在 token 裡,不記在伺服器,它有三段:標頭、內容(誰、何時簽的、何時過期)、簽章:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIyZTdhMWI2Yy0uLi4iLCJleHAiOjE3...}.4f3c9a...
     標頭                        內容(可以直接解開來看)              簽章

中間那段不是加密的,任何人都解得開,它的安全性完全來自第三段:只有持有 secret 的人簽得出對的簽章,所以內容一旦被改,驗證就會失敗。

JWT 不是「看不到內容」,是「改不動內容」。 所以裡面不要放密碼、不要放不該被看到的東西。

不記狀態換來的是三個必須自己決定的問題:

問題 這個專案的決定
多久過期? 12 小時。太短使用者一直被登出,太長被偷走的 token 有效期就長
要不要 refresh token? 本次不做
怎麼撤銷? 撤銷不了。 這是無狀態的代價——token 沒過期之前,伺服器沒有辦法讓它失效

RBAC:你的權限是「角色」還是「關係」

RBAC(Role-Based Access Control)是最常見的權限模型:開一張角色表、一張權限表、一張關聯表,然後問「這個人有沒有這個角色」。

一講到權限,多數人的預設反應就是它。但我在這個專案 grep 了一次:

role / Role / permission / Permission  →  0 處

一個角色都沒有。而權限檢查確實存在,如下:

if note.author_id != user.id:                          # 你是不是作者
    raise errors.not_note_author()

if user.id not in required_confirmer_ids(db, version_id):   # 你在不在名單裡
    raise errors.not_required_confirmer()

這兩個都不是角色,是關係。

問的問題 例子
角色 這個是什麼 管理員可以停用帳號
關係 這個人跟這份資料是什麼 作者可以編輯自己的草稿

兩者的差別:角色跟資料無關(管理員對每一份記錄都是管理員),而關係跟資料有關(你是這份文件的作者,但不是另一份的作者)。

硬把關係套進 RBAC 會變成「每一份記錄都要動態產生一個角色」——那只是把關係換個名字,還多一層。

RBAC 划算的時機:有一群人共享同一組權限,而且那組權限跟特定資料無關
這個系統目前沒有那種東西,所以不做。


交給 AI 做,可能會遇到的三個問題

1.它會照慣例補完你沒要的東西

AI 產規格的時候,遇到沒講清楚的地方會自己補,之前有說過它的判準是「業界有沒有慣例」。這在大部分功能上是好事,在安全功能上剛好相反——因為安全領域的慣例特別多,但慣例不等於你的系統需要。

叫它「做登入」,很可能連帶拿到 RBAC、refresh token、註冊流程、忘記密碼。每一項單獨看都合理,但每一項都是你沒有要求、之後要維護、而且可能永遠用不到的東西。

做法:把不做的東西連同理由寫進規格,跟要做的放在一起。

- 不做 RBAC。這個系統 role / permission 零命中,授權全部是關係式的。
- 不做註冊。功能清單裡沒有。
- refresh token 這輪不做。理由與當初押後登入一樣:標準做法、風險低。
  而這個理由要寫下來,失效條件才跟著存在。

2.安全測試容易假綠

功能需求講「做得到什麼」,安全需求講「做不到什麼」。把這次的驗收條件排在一起看:

  • 密碼以明文存在資料庫
  • 「查無此帳號」與「密碼錯誤」不能有任何差別
  • 沒帶 token 不能通過
  • 壞掉的 token 不能通過

全部是否定句。 而否定句有一個要命的性質:

斷言「不應該發生 X」的測試,對著一個還沒實作的程式,一律通過。

做法:先斷言正常路徑真的有東西,再驗有沒有「不存在」。

setToken('a.b.c')
expect(currentToken()).toBe('a.b.c')   // 守門:先證明它真的存得進去
clearToken()
expect(currentToken()).toBeNull()      // 這條現在才有意義

3.有些缺陷不會讓任何測試變紅,也不會讓功能不能用

這類東西 AI 不會主動提,因為從它的角度看一切正常。兩個實際的例子:

查無帳號也要花一樣的時間。

user = user_repository.get_by_email(db, email)
if user is None:
    security.verify_password(password, _DUMMY_HASH)   # ← 這行
    raise errors.invalid_credentials()

密碼雜湊是刻意設計成慢的。少了那行,「查無帳號」會明顯比「密碼錯誤」快回來——回應文字一模一樣,但時間差就足以列舉帳號

兩條路要長得一樣,不只是回應內容,連花的時間都要

「過期」和「無效」要分開。

兩個都是 401,很容易併成一個錯誤碼。但前端的處理完全不同:過期可以靜默重新登入,偽造的不行——那代表有人在試。合成一個碼,前端就只能選一種處理方式,而兩種選擇都是錯的。

做法:這類問題沒有自動化的守法,只能靠固定的提問清單。我現在會問這三個:

  • 失敗的兩條路,時間一樣嗎?
  • 錯誤訊息透露了什麼不該透露的?(帳號存不存在、哪個欄位錯)
  • 這個錯誤碼,前端會怎麼處理?兩種情況需要不同處理的話,就不能共用一個碼。

結論

  • 認證與授權是兩件事,而且可以獨立地真或假。
  • JWT 不是「看不到內容」,是「改不動內容」。
  • 先問你的權限是「角色」還是「關係」。 角色問「這個人是什麼」,關係問「這個人跟這份資料是什麼」。
  • 交給 AI 做認證與授權可能會遇到的三個問題:它會照慣例補你沒要的東西安全測試特別容易假綠有些缺陷不會讓任何測試變紅

明天:認證補上了,明天要處理這個系統最難的狀態:版本控管與簽核流。


上一篇
Day 20|資料庫與 Alembic:migration 的人工 review
下一篇
Day 22| 實用的 Claude 小技巧
系列文
30 天打造我的 AI 開發工作流:從需求分析到上線22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言